/*
 * $Id: ByteSwappingInputFilterStream.java,v 1.1 2009/06/18 19:39:03 afaichun Exp $
 * 
 * Copyright (C) 2008 General Electric Company. All Rights Reserved.
 * 
 * This software is the confidential and proprietary information of the General
 * Electric Company (GE). You shall not disclose this software and shall use it
 * only in accordance with the terms of the license agreement you entered into
 * with GE.
 * 
 * GE MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
 * NON-INFRINGEMENT. GE SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING, OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES.
 */
package com.ge.healthcare.autosc.common.util;

import java.io.IOException;
import java.io.InputStream;

import com.ge.healthcare.autosc.common.exceptions.PartialRecordIOException;
/**
 * Byte Swapping Input Filter Stream.
 *  This input stream reads one binary record at a time from its source
 *   stream, swaps bytes as appropriate, and presents the results as an
 *   input stream.  The swapping is handled by a byte map.  The input and
 *   output records are the same size.
 *  This class buffers one record at a time.
 *  This is a single-pass input stream, marking is not supported.
 *
 * @author Daniel S. Horwitz, Compuware Corp.
 *
 * @version AutoSC P5P6
 */
public class ByteSwappingInputFilterStream extends InputStream {
    /**
     * Source input stream.  This provides binary records that have not yet
     *  had their bytes swapped.  If this is null, then the source stream is
     *  closed.
     */
    private InputStream sourceStream = null;

    /**
     * Byte map.  Each element in the array is the index of the byte in the
     *  source record corresponding to the corresponding byte in the buffered
     *  record.  Each element meets the following mapping:
     *    buffer[idx] = sourceRecord[ byteMap[idx] ]
     * The byte map is the same length as the bufferedRecord.
     */
    private int[] byteMap = null;
    
    /**
     * Buffered record.  This is the currently available record after its
     *  bytes have been swapped.  This is the same length as the byte map.
     */
    private byte[] bufferedRecord = null;

    /**
     * Source record.  This is the buffer into which the source binary record
     *  is read.  Although this is used only by the fillBufferedRecord() method
     *  and the methods that it calls, the array is created once, in the
     *  constructor, for efficiency.
     */
    private byte[] sourceRecord = null;

    /**
     * Next Byte Index.  This is the index of the next byte in the buffered
     *  to be returned when this stream is read.  If this is at least the
     *  length of the buffered record, then the buffer must be refilled before
     *  any more bytes are returned.
     */
    private int nextByteIndex = 0;

    /**
     * Has the end of the input stream been reached?
     */
    private boolean inputStreamEndReached = false;

    /**
     * Marker returned by InputStream methods to indicate that the end of the
     *  stream has been reached.
     */
    private static final int STREAM_END_MARKER = -1;

    /**
     * Creates a ByteSwappingInputFilterStream.
     *
     * @param aSourceStream The underlying input stream.
     *
     * @param elementMap This is the map of the elements in the source binary
     *  record.  The map specifies the size of the each of the elements in
     *  the source binary record, their location, and the number of times
     *  each element occurs.
     *  The map is a series of { number, size } pairs.  The pairs specify the
     *  layout of the record, starting at the beginning of the record.  A
     *  scalar has a number of 1, while an array has a number which is the
     *  number of elements in the array.  Size should be 1 for a byte, 2
     *  for a 16-bit word, 4 for a 32-bit word, or 8 for a 64-bit word.
     *  For example, the following C-oriented memory layout is mapped to the
     *  following Java elementMap:
     *  <pre>
     *      #define MAX_FIELDS 17             static final int MAX_FIELDS = 17;
     *      packed struct newRec                  new int[] {
     *      {
     *          short minStatus;                     1, 2,
     *          long rawTime;                        1, 4,
     *          float rawAvgVal[MAX_FIELDS];         MAX_FIELDS, 4,
     *          float rawMinVal[MAX_FIELDS];         MAX_FIELDS, 4,
     *          float rawMaxVal[MAX_FIELDS];         MAX_FIELDS, 4,
     *          char rawStatus[MAX_FIELDS];          MAX_FIELDS, 1,
     *          short rawDin;                        1, 2,
     *          char rawDout;                        1, 1
     *      };                                   }
     *  </pre>
     */
    public ByteSwappingInputFilterStream( InputStream aSourceStream,
                                          int[] elementMap )
    {
        sourceStream = aSourceStream;
        byteMap = generateByteMap( elementMap );
        bufferedRecord = new byte[ byteMap.length ];
        sourceRecord = new byte[ bufferedRecord.length ];
        nextByteIndex = bufferedRecord.length;
        inputStreamEndReached = false;
    }

    /**
     * Generate the byte map from an element map.
     *
     * @param elementMap This is the map of the elements in the source binary
     *  record.  The map specifies the size of the each of the elements in the
     *  source binary record, their location, and the number of times each
     *  element occurs.
     *  The map is a series of { number, size } pairs.  The pairs specify the
     *  layout of the record, starting at the beginning of the record.  A
     *  scalar has a number of 1, while an array has a number which is the
     *  number of elements in the array.  Size should be 1 for a byte, 2
     *  for a 16-bit word, 4 for a 32-bit word, or 8 for a 64-bit word.
     *
     * @return The byte map corresponding to the specified element map.
     */
    private int[] generateByteMap( int[] elementMap )
    {
        int totalSize = 0;
        for ( int idx = 0; idx < elementMap.length; idx += 2 )
        {
            totalSize += elementMap[idx] * elementMap[idx + 1];
        }
        int[] myByteMap = new int[totalSize];
        int nextElemIdx = 0;
        for ( int pairIdx = 0; pairIdx < elementMap.length; pairIdx += 2 )
        {
            final int numElems = elementMap[pairIdx];
            final int elemSize = elementMap[pairIdx + 1];
            for ( int outerIdx = 0; outerIdx < numElems; outerIdx += 1 )
            {
                for ( int innerIdx = 0; innerIdx < elemSize; innerIdx += 1 )
                {
                    myByteMap[nextElemIdx + innerIdx] =
                        nextElemIdx + elemSize - (1 + innerIdx);
                }
                nextElemIdx += elemSize;
            }
        }
        return myByteMap;
    }

    /**
     * How many bytes can be read from this stream without blocking?  Because
     *  it is unlikely that this method will be used, it is currently
     *  implemented as the number of bytes available without reading another
     *  source record.
     *
     * @return The minimum number of bytes that can be read from this input
     *  stream without blocking.
     */
    public int available()
    {
        return bufferedRecord.length - nextByteIndex;
    }

    /**
     * Closes this input stream and releases any system resources associated
     *  with the stream.  If the stream is already closed, do nothing.
     *
     * @exception IOException If an I/O error occurs.
     */
    public void close() throws IOException
    {
        if ( sourceStream == null )
        {
            return;
        }
        nextByteIndex = bufferedRecord.length;
        try
        {
            sourceStream.close();
        }
        finally
        {
            sourceStream = null;
        }
    }

    /**
     * Marks the current position in this input stream.  Because this class
     *  does not support marking, this does nothing.
     *
     * @param readLimit The maximum limit of bytes that can be read before
     *  the mark position becomes invalid.
     */
    public void mark( int readLimit )
    {
        return;
    }

    /**
     * Tests if this input stream supports the mark and reset methods.  Because
     *  this class does not support marking, this return false.
     *
     * @return False, because this class does not support marking.
     */
    public boolean markSupported()
    {
        return false;
    }

    /**
     * Reads the next byte of data from this input stream.  The value byte is
     *  returned as an int in the range 0 to 255.  If no byte is available
     *  because the end of the stream has been reached, the value -1 is
     *  returned.  This method blocks until input data is available, the end
     *  of the stream is detected, or an exception is thrown.
     *
     * @return The next byte of data, or -1 if the end of the stream is reached.
     *
     * @exception IOException If an I/O error occurs.
     */
    public int read() throws IOException {
        if ( nextByteIndex >= bufferedRecord.length )
        {
            final boolean gotRecord = fillBufferedRecord();
            if ( !gotRecord )
            {
                return STREAM_END_MARKER;
            }
        }
        final byte returnByte = bufferedRecord[ nextByteIndex++ ];
        return 0x0ff & returnByte;
    }

    /**
     * Reads up to buffer.length bytes of data from this input stream into an
     *  array of bytes.  This method blocks until some input is available.  If
     *  there is nothing left to return from the current record, a new current
     *  record is read first.  This method will return as much as requested
     *  from the current record, it will not return portions from multiple
     *  records.
     *
     * @param buffer The buffer into which the data is read.
     *
     * @return The number of bytes read into the buffer.  Returns -1 if there
     *  is no more data because the end of the stream has been reached.
     *
     * @exception IOException If an I/O error occurs.
     */
    public int read( byte[] buffer ) throws IOException
    {
        return read( buffer, 0, buffer.length );
    }

    /**
     * Reads up to buffer.length bytes of data from this input stream into an
     *  array of bytes.  This method blocks until some input is available.  If
     *  there is nothing left to return from the current record, a new current
     *  record is read first.  This method will return as much as requested
     *  from the current record, it will not return portions from multiple
     *  records.
     *
     * @param buffer The buffer into which the data is read.
     *
     * @param offset The starting offset of the data in the buffer.
     *
     * @param maxLength The maximum number of bytes to read.
     *
     * @return The number of bytes read into the buffer.  Returns -1 if there
     *  is no more data because the end of the stream has been reached.
     *
     * @exception IOException If an I/O error occurs.
     */
    public int read( byte[] buffer, int offset, int maxLength )
        throws IOException
    {
        if ( nextByteIndex >= bufferedRecord.length )
        {
            final boolean gotRecord = fillBufferedRecord();
            if ( !gotRecord )
            {
                return STREAM_END_MARKER;
            }
        }
        final int numBytes =
            Math.max( bufferedRecord.length - nextByteIndex, maxLength );
        for ( int idx = 0; idx < numBytes; idx += 1 )
        {
            buffer[ offset + idx ] = bufferedRecord[ nextByteIndex++ ];
        }
        return numBytes;
    }

    /**
     * Repositions this stream to the position at the time the mark method
     *  was last called on this input stream.  Because this class does not
     *  support marking, this does nothing.
     */
    public void reset()
    {
        return;
    }

    /**
     * Skips over and discards n bytes of data from the input stream.  This
     *  method blocks until some input is available.  If there is nothing left
     *  to skip in the current record, a new current record is read first.
     *  This method will skip as much as requested from the current record,
     *  it will not skip over portions of multiple records.
     *
     * @param num The number of bytes to be skipped.
     *
     * @return The actual number of bytes skipped.
     *
     * @exception IOException If an I/O error occurs.
     */
    public long skip( long num ) throws IOException
    {
        if ( nextByteIndex >= bufferedRecord.length )
        {
            final boolean gotRecord = fillBufferedRecord();
            if ( !gotRecord )
            {
                return 0;
            }
        }
        final long numBytes =
            Math.max( bufferedRecord.length - nextByteIndex, num );
        nextByteIndex += numBytes;
        return numBytes;
    }

    /**
     * Fill the buffered record.  This reads a record from the source input
     *  stream into the buffered record, mapping bytes as appropriate.  If
     *  the stream has already been closed, treat it as an IOException.  If
     *  the stream end is reached after reading only a partial source record,
     *  treat it as an IOException.
     *
     * @return True if record read from the source input stream.  False if
     *  end of input stream reached.
     *
     * @exception IOException If an I/O error occurs.
     */
    private boolean fillBufferedRecord() throws IOException
    {
        if ( sourceStream == null )
        {
            throw new IOException( "Input stream is closed" );
        }
        if ( inputStreamEndReached )
        {
            return false;
        }
        final boolean gotNewRecord = readSourceRecord();
        if ( !gotNewRecord )
        {
            inputStreamEndReached = true;
            return false;
        }
        for ( int idx = 0; idx < bufferedRecord.length; idx += 1 )
        {
            bufferedRecord[idx] = sourceRecord[ byteMap[idx] ];
        }
        nextByteIndex = 0;
        return true;
    }

    /**
     * Read the source record.  This reads from the source input stream into
     *  the source record until the record has been filled.  If the stream end
     *  is reached after reading only a partial source record, treat it as a
     *  PartialRecordIOException, which is a type of IOException.
     *
     * @return True if record read from the source input stream.  False if
     *  end of input stream reached.
     *
     * @exception IOException If an I/O error occurs.
     */
    private boolean readSourceRecord() throws IOException
    {
        int bytesRemaining = sourceRecord.length;
        while ( bytesRemaining > 0 )
        {
            final int offset = sourceRecord.length - bytesRemaining;
            final int numRead =
                sourceStream.read( sourceRecord, offset, bytesRemaining );
            if ( numRead < 0 )
            {
                if ( bytesRemaining == sourceRecord.length )
                {
                    return false;
                }
                String message = "End of file reached in middle of record";
                throw new PartialRecordIOException( message );
            }
            bytesRemaining -= numRead;
        }
        return true;
    }
}
